/*
 *	Open Validation Library
 *	Copyright (c) 2011, Cameron Wardzala
 *
 *	Permission is hereby granted, free of charge, to any person obtaining a copy
 *	of this software and associated documentation files (the "Software"), to deal
 *	in the Software without restriction, including without limitation the rights
 *	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *	copies of the Software, and to permit persons to whom the Software is
 *	furnished to do so, subject to the following conditions:
 *
 *	The above copyright notice and this permission notice shall be included in
 *	all copies or substantial portions of the Software.
 *
 *	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *	THE SOFTWARE.
 */


/**
 * Core functions
 * A collection of reuseable functions for manipulation and traversal of the dom.
 * 
 * Unique parses an array and returns only unique results.
 * @param a
 * @returns Array with only unique members.
 * 
 * Find Index function when passed a DOM element and array of elements will find the index of the original element.
 * @param ele DOM element to be identified.
 * @param arr Array of DOM elements.
 * @returns int index of original element in array.
 * 
 * Extend function will take two objects and combine them keeping only the latest unique members.
 * @param obj The original object to be extended.
 * @param extObj The new object that will extend the original.
 * @returns Single combined object with unique memebers.
 * 
 * Has Class function will check if a DOM object has the specified class.
 * @param ele DOM element to be checked.
 * @param cls Class to be checked.
 * @returns true if it has the class, false if not.
 * 
 * Add Class function will add the specified class to the specified DOM object.
 * @param ele Element to be manipulated.
 * @param cls Class to be added.
 * 
 * Remove Class function will remove the specified class from the specified DOM object.
 * @param ele Element to be maniuplated.
 * @param class Class to be removed.
 * 
 * Return Label function will return text from the corresponding label for the given form field.
 * @param ele Form field.
 * @returns Text of corresponding label.
 * 
 * Prepend function will place the passed DOM object before the designated target.
 * @param ele DOM object to be placed.
 * @param target DOM object
 * 
 * Is Browser function will return true or false if specified browser is in use.
 * @param string The browser name to test for.
 * @returns true if browser is in use, false if not.
 * 
 */
var unique						= function (a) { var r = [],i,x,y,n; o:for(i = 0, n = a.length; i < n; i++){ for(x = 0, y = r.length; x < y; x++){ if(r[x]===a[i]) {continue o;} } r[r.length] = a[i]; } return r; };
var findIndex					= function (ele,arr) { var ctr = "",i; for (i=0; i < arr.length; i++) { if (arr[i] === ele) { return i; } } return ctr; };
var extend						= function (obj, extObj) { var i,a; if (arguments.length > 2) { for (a = 1; a < arguments.length; a++) { extend(obj, arguments[a]); } } else { for (i in extObj) { obj[i] = extObj[i]; } } return obj; };
var hasClass					= function (ele,cls) {return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); };
var addClass					= function (ele,cls) { if (!hasClass(ele,cls)) {ele.className += " "+cls;} };
var removeClass					= function (ele,cls) { if (hasClass(ele,cls)) { var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)'); ele.className=ele.className.replace(reg,' '); } };
var ReturnLabel					= function (ele) { if (ele !== undefined && ele.nodeType === 1 && ele.nodeName === "LABEL") { var text = ele.innerText || ele.textContent; return text.replace(/\([^)]* \)/g,'').replace(/[^a-zA-Z 0-9]+/g,'').replace(/^\s*|\s*$/g,'');} };
var prepend						= function (ele,target) { if ( target.nodeType === 1 ) { target.insertBefore( ele, target.firstChild );} };
var isBrowser					= function (string) {return (navigator.userAgent.indexOf(string)>=0);};

/**
 * Query function, when passed a scope object, and a string criteria in css syntax (id: #idname, class: .classname, element: div) will return an array of matched elements.
 * @param scope The DOM object to be queried, defaults to document.
 * @param string Class name, ID, or element name(s) in css format.
 * @returns An array of matched elements.
 */
var query = function (string,scope) {
	scope = scope || document;
	var es = string.replace(' ','').split(','), esl = es.length, retnode = [], elem = scope.getElementsByTagName('*'), el = elem.length, r=[],i,ei;
	for (i = 0; i < el; i++) {
		for (ei = 0; ei<esl; ++ei) {
			var type = es[ei].charAt(0), value = es[ei].replace(type,'');
			if (type === "#") {if (elem[i].id === value) {retnode.push(elem[i]);}}
			else if (type === ".") {var myclass = new RegExp(value+'(?=\s)|'+value+'(?!\s)'), classes = elem[i].className; if (myclass.test(classes)) {retnode.push(elem[i]);} }
			else { if (elem[i].nodeName === es[ei].toUpperCase()) { retnode.push(elem[i]);} }
		}
	}
	// Utilize the unique function rather than recreate it here.
	return unique(retnode);
};




/**
 * Rules
 */
var Rules = [
	{id:"isRequired", func: function (scope) { return (scope.value === null) || (scope.value.length !== 0);}, _class:"form-req", msg:"is a required field.", useLabels:true},
	{id:"isNumeric", func: function (scope) { var re = new RegExp(/^([0-9]\d*(\.|\,)\d*|0?(\.|\,)\d*[1-9]\d*|[0-9]\d*)$/); return re.test(scope.value);}, _class:"form-number", msg:"can only be a number.", useLabels:true},
	{id:"isEmail", func: function (scope) { var re = /^[^\s()<>@,;:\/]+@\w[\w\.\-]+\.[a-z]{2,}$/i;return re.test(scope.value);}, _class:"form-email", msg:"is an invalid email address.", useLabels:true},
	{id:"istheSame", func:function (scope) { var allSame = query('.form-same'), allSameVerify = query('.form-same_verify'), me = findIndex(scope,allSameVerify); if (scope.value !== allSame[me].value || scope.value === ""){this.msg = allSame[me].previousSibling.innerHTML+" and "+scope.previousSibling.innerHTML+" should be the same.";return false;}return true;}, _class:"form-same_verify", msg:"No Match", useLabels:false},
	{id:"isChecked", func: function (scope) { var checkElms = document.getElementsByName(scope.name), cei, chx = []; for (cei=0;cei<checkElms.length;++cei){ if (checkElms[cei].checked !== false) {chx.push(checkElms[cei]);} } if (chx.length > 0) {return true;} else { return false; } }, _class:"form-check", msg:"You Must Choose an Option", useLabels:false},
	{id:"isPhone", func: function (scope) {var re = /^(1\s*[\-\/\.]?)?(\((\d{3})\)|(\d{3}))\s*[\-\/\.]?\s*(\d{3})\s*[\-\/\.]?\s*(\d{4})\s*(([xX]|[eE][xX][tT])\.?\s*(\d+))*$/;return re.test(scope.value);}, _class:"form-phone", msg:"You must enter a valid phone number.", useLabels: false},
	{id:"isLength", func: function (scope) { var str = scope.value; var min = parseInt(scope.getAttribute("min"),10); var max = parseInt(scope.getAttribute("max"),10); if (min === max){ this.msg = "Your input must be "+max+" characters in length"; return (str.length === min); } else { this.msg = "Your input must be between "+min+" and "+max; return (str.length >= min) && (str.length <= max); } }, _class:"form-length", msg:"you must only enter ## characters", useLabels:false},
	{id:"isValue", func: function (scope) { var val = parseInt(scope.value,10), min = parseInt(scope.getAttribute("min"),10), max = parseInt(scope.getAttribute("max"),10); if (min === max) { return (val === min); } else { return (val >= min) && (val <= max); } }, _class:"form-value", msg:"test", useLabels:false}
];

/**
 * OpenVL class
 * @returns
 */
var OpenVL = function () {
	var ovl = this;
	this._options = {_form:document,_message:"",_allErrors:[],_focusBlur:true,uselabels:true,_msgType:"both"};
	this.validate = function (options) {
		ovl._options = extend(ovl._options, options);
		var forms = query(ovl._options._form), fL = forms.length;
		for (var fi=0;fi<fL;fi++) {
			if (ovl._options._focusBlur === true){
				var formInputs = query('input,select,textarea',forms[fi]), fil = formInputs.length;
				for(var i=0;i<fil;++i) {
					if(formInputs[i].getAttribute("type") !== "submit") {
						formInputs[i].onblur = (function(f) { return function(){ ovl._blur(this,f); }; })(forms[fi]);
						formInputs[i].onfocus = (function(f) { return function(){ ovl._focus(this,f); }; })(forms[fi]);
						if (isBrowser("Safari") && (formInputs[i].getAttribute("type") === "radio" || formInputs[i].getAttribute("type") === "checkbox")) {
							formInputs[i].onclick = (function(f) { return function(){ ovl._blur(this,f); }; })(forms[fi]);
						}
					}
				}
			}
			if (ovl._options._msgType === "both" || ovl._options._msgType === "list") {
				if (query('.MessageArea',forms[fi]).length === 0) {
					var msgdiv = document.createElement('div');
					msgdiv.className = 'MessageArea';
					prepend(msgdiv, forms[fi]);
				}
			}
			if (forms[fi].nodeName === "FORM") { 
				var oldHandler = forms[fi].onsubmit;
				forms[fi].onsubmit = function(e) {
					if(oldHandler){
						var oldHandlerReturnValue = oldHandler();
						var ovlReturnValue = ovl.exec(this);
						if (oldHandlerReturnValue == true && ovlReturnValue == true) { return true; } 
						else { return false; }
					} 
					else { return ovl.exec(this); }
				}
			}
		}
		
	};
	this._do = function (scope) {
		var test, parent = scope.parentNode, label = query("label",parent), labelText = ReturnLabel(label[0]);
		if (ovl._options.uselabels === false) {labelText = '';}
		for (var r=0;r<Rules.length;++r){
			if (hasClass(scope,Rules[r]._class)){
				test = Rules[r].func(scope) && test;
				if (test === false) {
					var message = Rules[r].msg;
					if (scope.getAttribute("data-" + Rules[r]._class +"-message")) { ovl._options.message = scope.getAttribute("data-" + Rules[r]._class +"-message"); } 
					else if (Rules[r].useLabels === true) { ovl._options.message = labelText +" "+ message; } 
					else { ovl._options.message = message; }
					break;
				}
			}
		}
		return test;
	};
	this._blur = function (ele,form) {
		var parent = ele.parentNode, allErrorDivs = query('.err_box',form);
		if (hasClass(parent.parentNode,"group")){parent = parent.parentNode;}
		if (ovl._do(ele) === false && (hasClass(ele,"form-req") || ele.value !== "")){
			ovl.errors._build(ele,form);
			allErrorDivs = query('.err_box',form);
			if (allErrorDivs.length !== 0){ for (var errs=0; errs<allErrorDivs.length;++errs){ ovl.errors._hide(errs,form); } ovl.errors._show(0,form); } 
			else if (allErrorDivs.length === 0) { ovl.errors._show(0,form); }
		} else {
			ovl.errors._clear(ele,form);
			allErrorDivs = query('.err_box',form);
			if (allErrorDivs.length !== 0){ ovl.errors._show(0,form); }
		}
	};
	this._focus = function (ele,form) {
		var parent = ele.parentNode;
		if (hasClass(parent.parentNode,"group")){parent = parent.parentNode;}
		var allErrorDivs = query('.form_err_wrapper',form);
		var allerrs = query('.err_box',form);
		var eindex = findIndex(parent, allerrs);
		if (hasClass(parent,"err_box")){ 
			allerrs = query('.err_box',form);
			for (var errs=0; errs<allErrorDivs.length;++errs) { 
				ovl.errors._hide(errs,form); 
			} 
			ovl.errors._show(eindex,form);
		}
	};
	this.exec = function (scope) {
		if (typeof scope === "string") {scope = document.getElementById(scope);}
		var allI = query('input,select,textarea',scope), noerrors = true, ali = allI.length;
		for (var ai=0;ai<ali;++ai){
			var aielm = allI[ai];
			if (aielm.getAttribute("type") !== "submit" || aielm.getAttribute("type") !== "hidden" || aielm.nodeName === "BUTTON"){
				if (hasClass(aielm,'form-req') || aielm.value !== ""){
					if (ovl._do(aielm) === false){ ovl.errors._build(aielm,scope); noerrors=false;}
					else { ovl.errors._clear(aielm,scope); }
				}
			}
		}
		var allerrs = query('.err_box',scope);
		for (var errs=0; errs<allerrs.length;++errs) { ovl.errors._hide(errs,scope); }
		if (!noerrors) { ovl.errors._show(0,scope); }
		return noerrors;
	};
	this.test = function (scope) {
		if (typeof scope === "string") {scope = document.getElementById(scope);}
		var allI = query('input,select,textarea',scope), noerrors = true, ali = allI.length;
		for (var ai=0;ai<ali;++ai){
			var aielm = allI[ai];
			if (aielm.type !== "submit" || aielm.type !== "hidden"){
				if (hasClass(aielm,'form-req') || aielm.value !== ""){
					if (ovl._do(aielm) === false){ noerrors=false; }
				}
			}
		}
		return noerrors;
	};
	this.clearvalidation = function (scope) {
		var forms = query(scope), fL = forms.length;
		for (var fi=0;fi<fL;fi++) {
			var thisForm = forms[fi], formInputs = query('input,select,textarea',thisForm),input;
			for(var i=0;i<formInputs.length;++i) {
				if (ovl._options._focusBlur === true){
					input = formInputs[i]; input.onblur = ""; input.onfocus = "";
				}
				if(formInputs[i].getAttribute("type") != "submit") {
					ovl.errors._clear(formInputs[i],scope);
				}
			}
			if (forms[fi].nodeName === "FORM") { forms[fi].onsubmit = ""; }
		}
	};
	this.errors = {
		_build:function (scope,form) {
			ovl._options._allErrors.push(ovl._options.message);
			var parent = scope.parentNode, errorDiv, newel, newelinner;
			if (hasClass(parent.parentNode,"group")){parent = parent.parentNode;}
			if (!hasClass(parent,'err_box')){ addClass(parent, "err_box");}
			if (ovl._options._msgType === "both" || ovl._options._msgType === "inline"){
				if (query('.form_err_wrapper',parent).length === 0){
					newel = document.createElement("div");
					newel.className = "form_err_wrapper";
					newelinner = document.createElement("div");
					newelinner.className="form_err_msg";
					newel.appendChild(newelinner);
					parent.appendChild(newel);
				}
				errorDiv = query('.form_err_msg',parent)[0];
				errorDiv.innerHTML = ovl._options.message;
			}
			if (ovl._options._msgType === "both" || ovl._options._msgType === "list"){
				var theErrors = unique(ovl._options._allErrors),
					ErrorsHTML = '<ul>';
				for (var ei=0;ei<theErrors.length;++ei){ ErrorsHTML += '<li>'+theErrors[ei]+'</li>'; }
					ErrorsHTML += '</ul>';
				if (query('.MessageArea',form)) {query('.MessageArea',form)[0].innerHTML=ErrorsHTML;}
			}
		},
		_show:function (index,form) {
			if (ovl._options._msgType === "both" || ovl._options._msgType === "inline"){
				var errboxes = query('.err_box',form);
				query('.form_err_wrapper',errboxes[index])[0].style.display="block";
			}			
		},
		_hide:function (index,form) { if (ovl._options._msgType === "both"||ovl._options._msgType === "inline"){ var errboxes = query('.err_box',form); query('.form_err_wrapper',errboxes[index])[0].style.display="none"; } },
		_clear:function (scope,form) {
			var parent = scope.parentNode;
			if (hasClass(parent.parentNode,"group")){parent = parent.parentNode;}
			removeClass(parent, "err_box");
			if (ovl._options._msgType === "both"||ovl._options._msgType === "inline"){
				if (query('.form_err_wrapper',parent).length !== 0){
					parent.removeChild(query('.form_err_wrapper',parent)[0]);
				}
			}
			var ei = query('.err_box',form);
			if ((ei.length) === 0) {
				var ma = query('.MessageArea',form);
				if((ma.length) !== 0) {ma.innerHTML="";}
				ovl._options._allErrors=null;
				ovl._options._allErrors=[];
			}
		}
	};
};